import json
from collections.abc import Iterable
from uuid import uuid4

from django.core.files import File
from django.core.files.base import ContentFile
from django.utils import timezone
from rest_framework import serializers

from sysreptor.pentests.import_export.serializers.common import (
    ExportImportSerializer,
    FileExportImportSerializer,
    FileListExportImportSerializer,
    MultiFormatSerializer,
)
from sysreptor.pentests.models import (
    NoteType,
    PentestProject,
    ProjectNotebookPage,
    ProjectType,
    UploadedAsset,
    UploadedImage,
    UploadedProjectFile,
    UploadedUserNotebookFile,
    UploadedUserNotebookImage,
    UserNotebookPage,
)
from sysreptor.pentests.serializers.notes import ExcalidrawDataField
from sysreptor.users.models import PentestUser
from sysreptor.utils.history import bulk_create_with_history
from sysreptor.utils.utils import omit_keys


class NotebookPageExportImportSerializer(ExportImportSerializer):
    id = serializers.UUIDField(source='note_id')
    parent = serializers.UUIDField(source='parent.note_id', allow_null=True, required=False)
    excalidraw_data = ExcalidrawDataField(allow_null=True, required=False)

    class Meta:
        model = ProjectNotebookPage
        fields = [
            'id', 'created', 'updated',
            'type', 'title', 'text', 'checked', 'icon_emoji',
            'order', 'parent', 'excalidraw_data',
        ]
        extra_kwargs = {
            'created': {'read_only': False, 'required': False},
            'type': {'read_only': False, 'required': False},
            'icon_emoji': {'required': False},
        }


class NotebookPageListExportImportSerializer(serializers.ListSerializer):
    @property
    def linked_object(self):
        if project := self.context.get('project'):
            return project
        elif user := self.context.get('user'):
            return user
        else:
            raise serializers.ValidationError('Missing project or user reference')

    def create_instance(self, validated_data):
        note_data = omit_keys(validated_data, ['parent'])
        if isinstance(self.linked_object, PentestProject):
            return ProjectNotebookPage(project=self.linked_object, **note_data)
        else:
            return UserNotebookPage(user=self.linked_object, **note_data)

    def create(self, validated_data):
        # Check for note ID collisions and update note_id on collision
        existing_instances = list(self.linked_object.notes.all())
        existing_ids = set(map(lambda n: n.note_id, existing_instances))
        for n in validated_data:
            if n['note_id'] in existing_ids:
                old_id = n['note_id']
                new_id = uuid4()
                n['note_id'] = new_id
                for cn in validated_data:
                    if cn.get('parent', {}).get('note_id') == old_id:
                        cn['parent']['note_id'] = new_id

        # Create instances
        instances = [self.create_instance(omit_keys(d, ['excalidraw_data'])) for d in validated_data]
        if not instances:
            return instances

        excalidrawfile_model = instances[0]._meta.model.excalidraw_file.related.related_model

        excalidraw_files = []
        for i, d in zip(instances, validated_data, strict=False):
            if d.get('parent'):
                i.parent = next(filter(lambda e: e.note_id == d.get('parent', {}).get('note_id'), instances), None)
            if i.type == NoteType.EXCALIDRAW and (excalidraw_data := d.pop('excalidraw_data', None)):
                excalidraw_files.append(excalidrawfile_model(
                    linked_object=i,
                    file=ContentFile(content=json.dumps(excalidraw_data).encode(), name=f'excalidraw-{timezone.now().isoformat()}.json'),
                ))
        ProjectNotebookPage.objects.check_parent_and_order(instances)

        # Update order to new top-level notes: append to end after existing notes
        existing_toplevel_count = len([n for n in existing_instances if not n.parent])
        for n in instances:
            if not n.parent_id:
                n.order += existing_toplevel_count

        bulk_create_with_history(instances[0]._meta.model, instances)
        bulk_create_with_history(excalidrawfile_model, excalidraw_files)
        self.context['storage_files'].extend(map(lambda f: f.file, excalidraw_files))
        return instances


class NotesImageExportImportSerializer(FileExportImportSerializer):
    class Meta(FileExportImportSerializer.Meta):
        model = UploadedImage

    def get_model_class(self):
        linked_object = self.get_linked_object()
        return UploadedImage if isinstance(linked_object, PentestProject) else \
            UploadedUserNotebookImage if isinstance(linked_object, PentestUser) else \
            UploadedAsset if isinstance(linked_object, ProjectType) else \
            None

    def get_linked_object(self):
        if project := self.context.get('project'):
            return project
        elif user := self.context.get('user'):
            return user
        elif project_type := self.context.get('project_type'):
            return project_type
        else:
            raise serializers.ValidationError('Missing project or user reference')

    def get_path_in_archive(self, name):
        return str(self.context.get('import_id') or self.get_linked_object().id) + '-images/' + name

    def is_file_referenced(self, f):
        if isinstance(self.get_linked_object(), PentestProject):
            return self.get_linked_object().is_file_referenced(f, findings=False, sections=False, notes=True)
        else:
            return self.get_linked_object().is_file_referenced(f)


class NotesFileExportImportSerializer(FileExportImportSerializer):
    class Meta(FileExportImportSerializer.Meta):
        model = UploadedProjectFile

    def get_model_class(self):
        linked_object = self.get_linked_object()
        return UploadedProjectFile if isinstance(linked_object, PentestProject) else \
            UploadedUserNotebookFile if isinstance(linked_object, PentestUser) else \
            UploadedAsset if isinstance(linked_object, ProjectType) else \
            None

    def get_linked_object(self):
        if project := self.context.get('project'):
            return project
        elif user := self.context.get('user'):
            return user
        elif project_type := self.context.get('project_type'):
            return project_type
        else:
            raise serializers.ValidationError('Missing project or user reference')

    def get_path_in_archive(self, name):
        return str(self.context.get('import_id') or self.get_linked_object().id) + '-files/' + name


class NotesExportImportSerializerV1(ExportImportSerializer):
    id = serializers.UUIDField()
    notes = NotebookPageListExportImportSerializer(child=NotebookPageExportImportSerializer())
    images = FileListExportImportSerializer(child=NotesImageExportImportSerializer(), required=False)
    files = FileListExportImportSerializer(child=NotesFileExportImportSerializer(), required=False)

    class Meta:
        model = PentestProject
        fields = ['id', 'notes', 'images', 'files']

    def to_representation(self, instance):
        if isinstance(instance, PentestProject):
            self.context['project'] = instance
        elif isinstance(instance, PentestUser):
            self.context['user'] = instance
        self.Meta.model = PentestProject if self.context.get('project') else PentestUser

        out = super().to_representation(instance=instance)
        # Set parent_id = None for exported child-notes
        exported_ids = set(map(lambda n: n['id'], out['notes']))
        for n in out['notes']:
            if n['parent'] and n['parent'] not in exported_ids:
                n['parent'] = None
        return out

    def export_files(self, instance) -> Iterable[tuple[str, File]]:
        imgf = self.fields['images']
        yield from imgf.export_files(instance=list(imgf.get_attribute(instance).all()))

        ff = self.fields['files']
        yield from ff.export_files(instance=list(ff.get_attribute(instance).all()))

    def create(self, validated_data):
        if self.context.get('project') or self.context.get('user'):
            return self.create_project_user_notes(validated_data)
        elif self.context.get('project_type'):
            return self.create_project_type_default_notes(validated_data)
        else:
            raise serializers.ValidationError('Missing project or user reference')

    def create_project_user_notes(self, validated_data):
        # Check for file name collisions and rename files and update references
        linked_object = self.context.get('project') or self.context.get('user')
        existing_images = set(map(lambda i: i.name, linked_object.images.all()))
        for ii in validated_data['images']:
            i_name = ii['name']
            while ii['name'] in existing_images:
                ii['name'] = UploadedImage.objects.randomize_name(i_name)
                ii['name_internal'] = i_name
            if i_name != ii['name']:
                for n in validated_data['notes']:
                    n['text'] = n['text'].replace(f'/images/name/{i_name}', f'/images/name/{ii["name"]}')

        existing_files = set(map(lambda f: f.name, linked_object.files.all()))
        for fi in validated_data['files']:
            f_name = fi['name']
            while fi['name'] in existing_files:
                fi['name'] = UploadedProjectFile.objects.randomize_name(f_name)
                fi['name_internal'] = f_name
            if f_name != fi['name']:
                for n in validated_data['notes']:
                    n['text'] = n['text'].replace(f'/files/name/{f_name}', f'/files/name/{fi["name"]}')

        # Import notes
        notes = self.fields['notes'].create(validated_data['notes'])

        # Import images and files
        self.context.update({'import_id': validated_data['id']})
        self.fields['images'].create(validated_data.get('images', []))
        self.fields['files'].create(validated_data.get('files', []))

        return notes

    def create_project_type_default_notes(self, validated_data):
        project_type = self.context.get('project_type')

        # Check for file name collisions and rename files and update references
        existing_assets = set(map(lambda i: i.name, project_type.assets.all()))
        for a in validated_data.get('images', []) + validated_data.get('files', []):
            a_name = a['name']
            while a['name'] in existing_assets:
                a['name'] = UploadedAsset.objects.randomize_name(a_name)
                a['name_internal'] = a_name
            for n in validated_data['notes']:
                n['text'] = n['text'].replace(f'/images/name/{a_name}', f'/assets/name/{a["name"]}')
                n['text'] = n['text'].replace(f'/files/name/{a_name}', f'/assets/name/{a["name"]}')

        # Update formatting for default_notes
        notes = validated_data['notes']
        for n in list(notes):
            if n['type'] == NoteType.EXCALIDRAW:
                notes.remove(n)
                continue
            n['id'] = str(n.pop('note_id', uuid4()))
            parent = n.get('parent', {}).get('note_id')
            n['parent'] = str(parent) if parent else None

        # Check for note ID collisions and update note_id on collision
        existing_ids = set(map(lambda n: str(n.get('id')), project_type.default_notes))
        for n in notes:
            if (old_id := n['id']) in existing_ids:
                n['id'] = str(uuid4())
                for cn in notes:
                    if cn['parent'] == old_id:
                        cn['parent'] = n['id']

        # Update order to new top-level notes: append to end after existing notes
        ProjectNotebookPage.objects.check_parent_and_order(notes)
        existing_toplevel_count = len([n for n in project_type.default_notes if not n.get('parent')])
        for n in notes:
            if not n.get('parent'):
                n['order'] += existing_toplevel_count

        # Import default_notes
        project_type.default_notes = project_type.default_notes + notes
        project_type.save(update_fields=['default_notes'])

        # Import images and files
        self.context.update({'import_id': validated_data['id']})
        self.fields['images'].create(validated_data.get('images', []))
        self.fields['files'].create(validated_data.get('files', []))

        return notes


class NotesExportImportSerializer(MultiFormatSerializer):
    serializer_formats = {
        'notes/v1': NotesExportImportSerializerV1(),
    }
